package com.moralesce.gt.model;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import org.apache.commons.collections.CollectionUtils;

/**
 * The physical table where community cards and burn cards are dealt.
 * 
 * A board may be populated in a couple of ways:
 * 
 * <p>
 * Using constructors, a board can be initialized with flop and/or turn and/or
 * river cards in one shot.<br/>
 * 
 * When constructing a board, the first three cards will be assigned as the flop
 * cards. The fourth card will be assigned as the turn card. The fifth card will
 * be assigned as the river card. The same rules apply when constructing a Board
 * using lists or arrays. An 'empty' board (i.e. board with no cards) may be
 * constructed as well. If cards are provided, the board must be constructed
 * with either 3, 4 or 5 cards.
 * 
 * Board flop = Board.newInstance("AHKC8C");<br/>
 * 
 * Board flopAndTurn = Board.newInstance("AHKCTS9S");<br/>
 * 
 * Board flopAndTurnAndRiver = Board.newInstance("AHKCTS9S8S");<br/>
 * </p>
 * 
 * <p>
 * Boards may also be populated programmatically using the flop(), turn() and
 * river() methods.
 * </p>
 * 
 * @author Brandon Grenier
 */
public class Board {

	private List<Card> flop = null;
	private Card turn = null;
	private Card river = null;

	public static Board newInstance() {
		return new Board();
	}

	public static Board newInstance(String value) {
		if (value.length() < 6) {
			throw new IllegalArgumentException("A board cannot be created with less than 6 card characters.");
		}
		if (value.length() > 10) {
			throw new IllegalArgumentException("A board cannot be created with more than 10 card characters.");
		}

		List<Card> cards = new ArrayList<Card>();
		cards.add(new Card(String.valueOf(value.charAt(0)) + String.valueOf(value.charAt(1))));
		cards.add(new Card(String.valueOf(value.charAt(2)) + String.valueOf(value.charAt(3))));
		cards.add(new Card(String.valueOf(value.charAt(4)) + String.valueOf(value.charAt(5))));
		if (value.length() > 6) {
			cards.add(new Card(String.valueOf(value.charAt(6)) + String.valueOf(value.charAt(7))));
		}
		if (value.length() > 8) {
			cards.add(new Card(String.valueOf(value.charAt(8)) + String.valueOf(value.charAt(9))));
		}
		return new Board(cards);
	}

	public static Board newInstance(List<Card> cards) {
		return new Board(cards);
	}

	public static Board newInstance(Card[] cards) {
		return new Board(Arrays.asList(cards));
	}

	private Board() {

	}

	private Board(List<Card> cards) {
		if (cards.size() < 3) {
			throw new IllegalArgumentException("Cannot create a board with less than " + cards.size() + " cards - a board must have at least 3 cards.");
		}
		if (cards.size() > 5) {
			throw new IllegalArgumentException("Cannot create a board with more than " + cards.size() + " cards - a board must have at most 5 cards.");
		}

		flop = new ArrayList<Card>();
		flop.add(cards.get(0));
		flop.add(cards.get(1));
		flop.add(cards.get(2));

		if (cards.size() > 3) {
			this.turn = cards.get(3);
		}
		if (cards.size() > 4) {
			this.river = cards.get(4);
		}
	}

	/**
	 * Assigns the given flop to the board.
	 * 
	 * @param value The value of the flop cards to assign.
	 * 
	 * @return The board.
	 */
	public Board setFlop(String value) {
		if (value.length() != 6) {
			throw new IllegalArgumentException("A flop must be created with less than 6 card characters.");
		}

		List<Card> cards = new ArrayList<Card>();
		cards.add(new Card(String.valueOf(value.charAt(0)) + String.valueOf(value.charAt(1))));
		cards.add(new Card(String.valueOf(value.charAt(2)) + String.valueOf(value.charAt(3))));
		cards.add(new Card(String.valueOf(value.charAt(4)) + String.valueOf(value.charAt(5))));
		return setFlop(cards);
	}

	/**
	 * Assigns the given flop to the board.
	 * 
	 * @param cards The flop to assign.
	 * 
	 * @return The board, including the given flop.
	 */
	public Board setFlop(List<Card> cards) {
		if (cards == null || cards.size() != 3) {
			throw new IllegalArgumentException("The flop must contain exactly three cards.");
		}
		this.flop = cards;
		return this;
	}

	/**
	 * Returns the flop cards.
	 * 
	 * @return The flop cards, or null if the flop hasn't been dealt.
	 */
	public List<Card> getFlop() {
		return flop;
	}

	/**
	 * Assigns the given turn card to the board.
	 * 
	 * @param value The value of the turn card to assign.
	 * 
	 * @return The board, including the given turn card.
	 */
	public Board setTurn(String value) {
		return setTurn(new Card(value));
	}

	/**
	 * Assigns the given turn card to the board.
	 * 
	 * @param card The turn card to assign.
	 * 
	 * @return The board, including the given turn card.
	 */
	public Board setTurn(Card card) {
		if (card == null) {
			throw new NullPointerException("Turn card is null");
		}
		if (flop == null) {
			throw new IllegalStateException("Cannot deal turn card, the flop has not been dealt.");
		}
		this.turn = card;
		return this;
	}

	/**
	 * Returns the turn card.
	 * 
	 * @return The turn card, or null if the turn card hasn't be dealt.
	 */
	public Card getTurn() {
		return turn;
	}

	/**
	 * Assigns the given river card to the board.
	 * 
	 * @param value The value of the river card to assign.
	 * 
	 * @return The board, including the given river card.
	 */
	public Board setRiver(String value) {
		return setRiver(new Card(value));
	}

	/**
	 * Assigns the given river card to the board.
	 * 
	 * @param card The river card to assign.
	 * 
	 * @return The board, including the given river card.
	 */
	public Board setRiver(Card card) {
		if (turn == null) {
			throw new IllegalStateException("Cannot deal river card, turn card has not been dealt.");
		}
		if (card == null) {
			throw new NullPointerException("River card is null");
		}
		this.river = card;
		return this;
	}

	/**
	 * Returns the river card.
	 * 
	 * @return The river card, or null if the river card hasn't be dealt.
	 */
	public Card getRiver() {
		return river;
	}

	/**
	 * Returns all cards on the board.
	 * 
	 * This method returns a list containing flop, turn and river cards (in the
	 * given order).
	 * 
	 * @return A List<Card> of all cards on the board.
	 */
	public List<Card> getCards() {
		List<Card> cards = new ArrayList<Card>();
		if (flop != null) {
			cards.addAll(flop);
		}
		if (turn != null) {
			cards.add(turn);
		}
		if (river != null) {
			cards.add(river);
		}
		return Collections.unmodifiableList(cards);
	}

	/**
	 * Determines if this board contains any of the cards in the given list.
	 * 
	 * @param cards The list of cards.
	 * 
	 * @return true, if the board contains at least one card in the given list
	 * of cards, false otherwise.
	 */
	public boolean containsAny(List<Card> cards) {
		return (cards == null || cards.isEmpty()) ? false : CollectionUtils.containsAny(this.getCards(), cards);
	}

}
